<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the PDORM_Record class.
 *
 * PHP version 5
 *
 * This file is part of PEAR::PDORM - A database object relational management
 * layer for PHP 5 built using PDO. PHP version 5.1+ is recommended.
 *
 * PEAR::PDORM is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * PEAR::PDORM is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::PDORM. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: Record.php 41 2007-12-11 23:00:36Z mjijackson $
 * @link        http://pear.php.net/packages/PDORM
 */

/**
 * The base class for all domain objects. Uses lazy loading to minimize strain
 * on system resources.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
abstract class PDORM_Record implements ArrayAccess
{

    /**#@+
     * @access  protected
     */
    /**
     * Tells whether this object is 'dirty'. Dirty objects have been altered
     * and need to be saved to the database.
     *
     * @var     bool
     */
    protected $_dirty = false;

    /**
     * Tells whether this object is 'temporary'. Temporary objects will not
     * be saved to the database.
     *
     * @var     bool
     */
    protected $_temporary = false;

    /**
     * Tells whether this object is already in the database. Records that are
     * already in the database will be saved using UPDATE instead of INSERT.
     *
     * @var     bool
     */
    protected $_already_in_db = false;

    /**
     * Contains this object's properties.
     *
     * @var     array
     */
    protected $_properties = array();
    /**#@-*/

    /**
     * Constructor.
     *
     * @param   array   $attributes     An array of initial attributes for this
     *                                  object
     * @param   bool    $already_in_db  True if this object came from the database
     * @access  public
     */
    public function __construct($attributes = array(), $already_in_db = false)
    {
        $mapper = $this->mapper();

        // set any attributes
        $field_names = $mapper->fieldNames();
        foreach ($field_names as $name) {
            if (array_key_exists($name, $attributes)) {
                $this->_properties[$name] = $attributes[$name];
            } else {
                $this->_properties[$name] = null;
            }
        }

        // set the object's id (primary key)
        $p_key = $mapper->primaryKey();
        if (!isset($this->_properties[$p_key])) {
            $this->_properties[$p_key] = $mapper->newId();
        }

        // set up dummy properties for objects and collections here?

        // is it already in the database?
        if ($already_in_db) {
            $this->_already_in_db = true;
            $this->markClean();
        }

        // add to the domain cache
        PDORM::add($this);
    }

    /**
     * Used to intercept direct property accessing on this object. Instead of
     * accessing a property of this object directly, this method will search
     * the internal {@link $_properties} array for the requested property.
     *
     * @param   string  $property       The property being accessed
     * @return  mixed                   This object's property
     * @access  public
     */
    public function __get($property)
    {
        if ($value = $this->get($property)) {
            return $value;
        }

        trigger_error(sprintf('Undefined property: %s::$%s.', get_class($this),
            $property), E_USER_ERROR);
    }

    /**
     * Used to intercept direct property setting on this object. Instead of
     * setting the property on this object directly, this method will search
     * the internal {@link $_properties} array for the property and set it
     * accordingly.
     *
     * @param   string  $property       The property being set
     * @param   mixed   $value          The value to set the property to
     * @return  void
     * @access  public
     */
    public function __set($property, $value)
    {
        if ($this->set($property, $value)) {
            return;
        }

        trigger_error(sprintf('Undefined property: %s::$%s.', get_class($this),
            $property), E_USER_ERROR);
    }

    /**
     * Supports get/set methods on this object where the method name will be
     * get/set followed by the property name. The property name will be passed
     * directly to this object's mapper's {@link PDORM_Mapper::fieldName() fieldName()}
     * method, where it will be resolved to a database field name.
     *
     * @param   string  $method     The method that was called on the object
     * @param   array   $args       The arguments that were passed to the method
     * @return  mixed               The property if get, void otherwise
     * @access  public
     */
    public function __call($method, $args)
    {
        // is it a setter or getter method?
        if (preg_match('/^(set|get)(\w+)/i', $method, $match)) {
            $property = $this->mapper()->fieldName($match[2]);

            switch (strtolower($match[1])) {
                case 'get':
                    return $this->get($property);
                break;
                case 'set':
                    return $this->set($property, $args[0]);
                break;
            }
        }

        trigger_error(sprintf('Call to undefined function: %s::%s().',
            get_class($this), $method), E_USER_ERROR);
    }

    /**
     * Gets the name of this object's domain class.
     *
     * @return  string          The name of this object's domain class
     * @access  public
     */
    public function domainClass()
    {
        return get_class($this);
    }

    /**
     * Gets the instance of this object's domain mapper.
     *
     * @return  PDORM_Mapper     This object's mapper
     * @access  public
     */
    public function mapper()
    {
        return PDORM::mapperFactory($this->domainClass());
    }

    /**
     * Gets a property of this domain object.
     *
     * @param   string  $property           The name of the property to get
     * @return  mixed                       The property's value
     * @throws  PDORM_RelationException     If the object does not have a
     *                                      defined relation to the collection
     * @access  public
     */
    public function get($property)
    {
        $property = strtolower($property);

        // is it already set (could be a field)?
        if (isset($this->_properties[$property])) {
            return $this->_properties[$property];
        }

        $mapper = $this->mapper();

        // is it a child/parent object?
        if (in_array($property, $mapper->objectNames($property))) {
            $obj_mapper = PDORM::mapperFactory($property);

            $this->_properties[$property] = $obj_mapper->find(
                $this->_properties[$mapper->foreignKey($property)],
                $mapper->findOptions($property)
            );

            return $this->_properties[$property];
        }

        // is it a collection?
        if ($singular = array_search($property, $mapper->collectionNames())) {
            $coll_mapper = PDORM::mapperFactory($singular);

            $id = $this->getId();
            $options = $mapper->findOptions($singular);
            if (empty($options['where'])) {
                $options['where'] = array();
            }

            // is it many to many?
            if ($join_table = $mapper->joinTable($singular)) {
                $mapper_conn = $mapper->conn();
                $join_table = $mapper_conn->quoteIdentifier($join_table, true);
                $p_key = $coll_mapper->primaryKey();
                $f_key = $mapper->foreignKey($singular);
                $of_key = $mapper->ownForeignKey($singular);
                $options['where'][] = sprintf("$p_key IN (%s)",
                    $mapper_conn->subQuery("SELECT $f_key FROM $join_table WHERE $of_key = $id"));
            } else {
                $f_key = $coll_mapper->foreignKey($this->domainClass());
                $options['where'][] = "$f_key = $id";
            }

            $this->_properties[$property] = $coll_mapper->find($options);

            return $this->_properties[$property];
        }

        return null;
    }

    /**
     * Updates a property of this object with the given value.
     *
     * @param   string  $property   The property to update
     * @param   mixed   $value      The new value for this relation
     * @return  bool                True if the property was set successfully,
     *                              false otherwise
     * @access  public
     */
    public function set($property, $value)
    {
        $mapper = $this->mapper();

        // is it a field?
        if (in_array($property, $mapper->fieldNames())) {
            $this->_properties[$property] = $value;
            $this->_dirty = true;
            return true;
        }

        // is it an object?
        if (in_array($property, $mapper->objectNames())) {
            // perform type checking
            if (($value instanceof $property) || is_null($value)) {
                $this->_properties[$property] = $value;
                $this->_dirty = true;
                return true;
            } else {
                throw new PDORM_TypeException(get_class($value), $property);
            }
        }

        // is it a collection?
        $singular = array_search($property, $mapper->collectionNames());
        if ($singular !== false) {
            // perform type checking
            if ((($value instanceof PDORM_Collection)
                && strcasecmp($value->objectClass(), $singular) == 0)
                || is_null($value)) {
                    $this->_properties[$property] = $value;
                    $this->_dirty = true;
                    return true;
            } else {
                throw new PDORM_TypeException(get_class($value), 'PDORM_Collection');
            }
        }

        return false;
    }

    /**
     * Saves this object to the database.
     *
     * @return  void
     * @access  public
     */
    public function save()
    {
        return $this->mapper()->save($this);
    }

    /**
     * Deletes this object from the database.
     *
     * @return  void
     * @access  public
     */
    public function delete()
    {
        return $this->mapper()->delete($this);
    }

    /**
     * Gets the primary key value for this object.
     *
     * @return  int         The object's primary key value
     * @access  public
     */
    public function getId()
    {
        return $this->_properties[$this->mapper()->primaryKey()];
    }

    /**
     * Determines whether this object's properties have been altered since it
     * was last saved.
     *
     * @return  bool        True if this object is dirty
     * @access  public
     * @see     $_dirty
     */
    public function isDirty()
    {
        return $this->_dirty;
    }

    /**
     * Marks this object as 'clean' meaning it will not be scheduled for
     * automatic save to the database.
     *
     * @return  void
     * @access  public
     * @see     $_dirty
     */
    public function markClean()
    {
        $this->_dirty = false;
    }

    /**
     * Determines whether this object is temporary.
     *
     * @return  bool        True if this is a temporary object
     * @access  public
     * @see     $_temporary
     */
    public function isTemporary()
    {
        return $this->_temporary;
    }

    /**
     * Marks this object as 'temporary' meaning it will not be scheduled for
     * automatic insertion into the database.
     *
     * @return  void
     * @access  public
     * @see     $_temporary
     */
    public function markTemporary()
    {
        $this->_temporary = true;
    }

    /**
     * Determines whether this object is already in the database.
     *
     * @return  bool        True if the object is already in the database
     * @access  public
     * @see     $_already_in_db
     */
    public function isAlreadyInDb()
    {
        return $this->_already_in_db;
    }

    /**
     * Marks this object as already being in the database. This is called
     * automatically on save.
     *
     * @return  void
     * @access  public
     * @see     $_already_in_db
     */
    public function markAlreadyInDb()
    {
        $this->_already_in_db = true;
    }

    /**
     * Determines if the specified property exists in this object.
     *
     * @param   mixed   $offset     The property to check
     * @return  bool                True if the specified offset exists
     * @access  public
     * @link    http://www.php.net/~helly/php/ext/spl/interfaceArrayAccess.html ArrayAccess
     */
    public function offsetExists($offset)
    {
        return !is_null($this->get($offset));
    }

    /**
     * Gets the value of the specified property in this object.
     *
     * @param   mixed   $offset     The property to get
     * @return  mixed               The element at the specified offset
     * @access  public
     * @link    http://www.php.net/~helly/php/ext/spl/interfaceArrayAccess.html ArrayAccess
     */
    public function offsetGet($offset)
    {
        return $this->offsetExists($offset) ? $this->get($offset) : null;
    }

    /**
     * Sets the given property on this object to the given value. Implicitly
     * throws PDORM_TypeException if the value is an object of the wrong
     * type. Triggers E_USER_ERROR if the offset is not defined.
     *
     * @param   int     $offset     The property to set
     * @param   mixed   $value      The value
     * @return  bool                True on success, false on failure
     * @throws  PDORM_TypeException     If the given object does is not of
     *                                  the correct type for this collection
     * @access  public
     * @link    http://www.php.net/~helly/php/ext/spl/interfaceArrayAccess.html ArrayAccess
     */
    public function offsetSet($offset, $value)
    {
        if (!is_null($offset)) {
            return $this->set($offset, $value);
        }

        trigger_error('No property name specified.', E_USER_ERROR);
        return false;
    }

    /**
     * Unsets the specified property of this object.
     *
     * @param   mixed   $offset     The property to unset
     * @return  void
     * @access  public
     * @link    http://www.php.net/~helly/php/ext/spl/interfaceArrayAccess.html ArrayAccess
     */
    public function offsetUnset($offset)
    {
        return $this->set($offset, null);
    }

}

?>
